package core.xml;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import core.util.IndentWriter;

public class TreeNode {
	/**
	 * The attributes of this node, keyed by attribute name,
	 * Instantiated only if required.
	 */
	protected Map<String, String> attributes = null;

	/**
	 * The body text associated with this node (if any).
	 */
	protected String body = null;

	/**
	 * The children of this node, instantiated only if required.
	 */
	protected List<TreeNode> children = null;

	/**
	 * The name of this node.
	 */
	protected String name = null;

	/**
	 * The parent node of this node.
	 */
	protected TreeNode parent = null;

	public TreeNode(String name) {
		this(name, null);
	}

	/**
	 * Construct a new node with the specified parent.
	 *
	 * @param name The name of this node
	 * @param parent The node that is the parent of this node
	 */
	public TreeNode(String name, TreeNode parent) {
		super();
		this.name = name;
		this.parent = parent;
		if (this.parent != null) this.parent.addChild(this);
	}

	/**
	 * Add an attribute to this node, replacing any existing attribute
	 * with the same name.
	 *
	 * @param name The attribute name to add
	 * @param value The new attribute value
	 */
	public void addAttribute(String name, String value) {
		if (attributes == null) {
			attributes = new HashMap<String, String>();
		}
		
		attributes.put(name, value);
	}

	public void addAttributes( Map<String, String> attrs ) {
		if (attributes == null) {
			attributes = new HashMap<String, String>();
		}
		
		attributes.putAll( attrs );
	}

	/**
	 * Add a new child node to this node.
	 *
	 * @param node The new child node
	 */
	public void addChild(TreeNode node) {
		if (children == null) {
			children = new ArrayList<TreeNode>();
		}

		children.add(node);
	}

	public void addChildren( Collection<TreeNode> nodelist ) {
		if ( children == null ) {
			children = new ArrayList<TreeNode>();
		}
		
		children.addAll( nodelist );
	}

	/**
	 * Return the value of the specified node attribute if it exists, or
	 * <code>null</code> otherwise.
	 *
	 * @param name Name of the requested attribute
	 */
	public String findAttribute(String name) {
		if (attributes == null) return (null);
		else return ((String) attributes.get(name));
	}

	/**
	 * Return an Map of the attribute names of this node.  If there are
	 * no attributes, an empty Iterator is returned.
	 */
	public Map<String, String> getAttributes() {
		if (attributes == null) {
			attributes = new HashMap<String, String>();
		}
		
		return attributes;
	}

	/**
	 * Return the first child node of this node with the specified name,
	 * if there is one; otherwise, return <code>null</code>.
	 *
	 * @param name Name of the desired child element
	 */
	public TreeNode findChild(String name) {
		if (children == null) return (null);

		Iterator<TreeNode> items = children.iterator();

		while (items.hasNext()) {
			TreeNode item = (TreeNode) items.next();

			if (name.equals(item.getName())) return (item);
		}

		return (null);
	}

	/**
	 * Return an ArrayList of all children of this node.  If there are no
	 * children, an empty Iterator is returned.
	 */
	public List<TreeNode> getChildren() {
		if (children == null) {
			children = new ArrayList<TreeNode>();
		}
		
		return children;
	}

	/**
	 * Return an Iterator over all children of this node that have the
	 * specified name.  If there are no such children, an empty Iterator
	 * is returned.
	 *
	 * @param name Name used to select children
	 */
	public List<TreeNode> findChildren(String name) {
		if (children == null) {
			children = new ArrayList<TreeNode>();
		}

		List<TreeNode> results = new ArrayList<TreeNode>();
		Iterator<TreeNode> items = children.iterator();
		while (items.hasNext()) {
			TreeNode item = items.next();
			if (name.equals(item.getName())) results.add(item);
		}

		return results;
	}

	/**
	 * Return the body text associated with this node (if any).
	 */
	public String getBody() {
		return (this.body);
	}

	/**
	 * Return the name of this node.
	 */
	public String getName() {
		return (this.name);
	}

	/**
	 * Remove any existing value for the specified attribute name.
	 *
	 * @param name The attribute name to remove
	 */
	public void removeAttribute(String name) {
		if (attributes != null) attributes.remove(name);
	}

	/**
	 * Remove a child node from this node, if it is one.
	 *
	 * @param node The child node to remove
	 */
	public void removeNode(TreeNode node) {
		if (children != null) children.remove(node);
	}

	/**
	 * Set the body text associated with this node (if any).
	 *
	 * @param body The body text (if any)
	 */
	public void setBody(String body) {
		this.body = body;
	}

	public String toString() {
		return toXMLString();
	}

	/**
	 * Return a String representation of this TreeNode.
	 */
	public String toXMLString() {
		StringWriter out = new StringWriter();
		CommonWriter cout = new CommonWriter( out, false );

		try {
			writeXMLStringImpl( cout, this );
		}
		catch( IOException e ) {
		}

		return out.toString();
	}

	public String toFormattedXMLString() {
		StringWriter out = new StringWriter();
		CommonWriter cout = new CommonWriter( new IndentWriter(out), true );

		try {
			writeXMLStringImpl( cout, this );
		}
		catch( IOException e ) {
		}

		return out.toString();
	}

	public void writeXMLString( Writer out ) throws IOException {
		CommonWriter cout = new CommonWriter( new IndentWriter(out), false );
		writeXMLStringImpl( cout, this );
	}

	public void writeFormattedXMLString( Writer out ) throws IOException {
		CommonWriter cout = new CommonWriter( out, true );
		writeXMLStringImpl( cout, this );
	}

	public void writeFormattedXMLString( IndentWriter out ) throws IOException {
		CommonWriter cout = new CommonWriter( out, true );
		writeXMLStringImpl( cout, this );
	}

	/**
	 * Append to the specified StringBuffer a character representation of
	 * this node, with the specified amount of indentation.
	 *
	 * @param sb The StringBuffer to append to
	 * @param indent Number of characters of indentation
	 * @param node The TreeNode to be printed
	 */
	private void writeXMLStringImpl( CommonWriter out, TreeNode node ) throws IOException {
		out.write( '<' );
		out.write( node.getName() );

		Iterator<String> names = node.getAttributes().keySet().iterator();
		while( names.hasNext() ) {
			out.write( ' ' );
			String name = (String)names.next();
			out.write( name );
			out.write( "=\"" );
			String value = node.findAttribute(name);
			out.write( value );
			out.write( "\"" );
		}

		out.write( '>' );

		// Reconstruct the body text of this node (if any)
		String body = node.getBody();
		if ( body != null && body.length() > 0 ) {
			out.write( body );
		}

		// Reconstruct child nodes with extra indentation
		Iterator<TreeNode> children = node.getChildren().iterator();
		if ( children.hasNext() == true ) {
			out.newLine();
		}

		while (children.hasNext()) {
			TreeNode child = (TreeNode) children.next();
			out.indentIn();
			writeXMLStringImpl( out, child );
			out.indentOut();
			out.newLine();
		}

		out.write( "</" );
		out.write( node.getName() );
		out.write( '>' );
	}

	public void setName(String name) {
		this.name = name;
	}

	private class CommonWriter {
		private Writer out;

		private IndentWriter iout;

		private boolean formatFlag = false;

		public CommonWriter( Writer out, boolean formatFlag ) {
			this.out = out;
			this.formatFlag = formatFlag;
			if ( formatFlag == true ) {
				iout = (IndentWriter )out;
			}
		}

		public void write( String str ) throws IOException {
			out.write( str );
		}

		public void write( int c ) throws IOException {
			out.write( c );
		}

		public void newLine() throws IOException {
			if ( formatFlag == false ) return ;
			out.write( '\n' );
		}

		public void indentIn() throws IOException {
			if ( formatFlag == false ) return ;
			iout.indentIn();
		}

		public void indentOut() throws IOException {
			if ( formatFlag == false ) return ;
			iout.indentOut();
		}
	}
}